package registry

import (
	"bufio"
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"runtime"
	"strings"

	"github.com/containers/common/pkg/retry"
	"github.com/containers/image/v5/copy"
	"github.com/containers/image/v5/docker/reference"
	"github.com/containers/image/v5/signature"
	"github.com/containers/image/v5/signature/signer"
	"github.com/containers/image/v5/transports/alltransports"
	"github.com/containers/image/v5/types"
	encconfig "github.com/containers/ocicrypt/config"

	"gopkg.openfuyao.cn/bkeadm/pkg/root"
	"gopkg.openfuyao.cn/bkeadm/utils"
	"gopkg.openfuyao.cn/bkeadm/utils/log"
)

type Options struct {
	root.Options
	Args []string `json:"args"`

	// sync
	File          string `json:"file"`
	Source        string `json:"source"`
	Target        string `json:"target"`
	MultiArch     bool   `json:"multi-arch"`
	SrcTLSVerify  bool   `json:"src-tls-verify"`
	DestTLSVerify bool   `json:"dest-tls-verify"`
	SyncRepo      bool   `json:"sync-repo"`

	// transfer
	Arch  string `json:"arch"`
	Image string `json:"image"`

	// view
	Prefix string `json:"prefix"`
	Tags   int    `json:"tags"`
	Export bool   `json:"export"`
}

func (op *Options) Sync() {
	if op.SyncRepo {
		newOp := *op
		if err := syncRepo(newOp); err != nil {
			log.BKEFormat(log.ERROR, fmt.Sprintf("Sync repo %s to %s error %s", op.Source, op.Target, err.Error()))
			return
		}
		log.BKEFormat(log.INFO, fmt.Sprintf("Sync repo %s to %s success", op.Source, op.Target))
		return
	}

	if len(op.File) == 0 {
		newOp := *op
		archs := []string{}
		if op.MultiArch {
			archs = append(archs, []string{"amd64", "arm64"}...)
		} else {
			if len(op.Arch) > 0 {
				archs = append(archs, op.Arch)
			} else {
				archs = append(archs, runtime.GOARCH)
			}
		}
		err := syncRepoImage(newOp, archs)
		if err != nil {
			log.Error(err.Error())
			return
		}
		return
	}

	imageList := []string{}

	fi, err := os.Open(op.File)
	if err != nil {
		log.Error(err.Error())
		return
	}
	buf := bufio.NewScanner(fi)
	for {
		if !buf.Scan() {
			break
		}
		if len(buf.Text()) == 0 {
			continue
		}
		imageList = append(imageList, buf.Text())
	}

	newOp := *op
	sourceAddress := newOp.Source
	targetAddress := newOp.Target
	if !strings.HasSuffix(sourceAddress, "/") {
		sourceAddress = sourceAddress + "/"
	}
	if !strings.HasSuffix(targetAddress, "/") {
		targetAddress = targetAddress + "/"
	}

	for _, image := range imageList {
		newOp.Source = sourceAddress + image
		newOp.Target = targetAddress + image
		archs := []string{}
		if op.MultiArch {
			archs = append(archs, []string{"amd64", "arm64"}...)
		} else {
			if len(op.Arch) > 0 {
				archs = append(archs, op.Arch)
			} else {
				archs = append(archs, runtime.GOARCH)
			}
		}
		err := syncRepoImage(newOp, archs)
		if err != nil {
			log.Error(err.Error())
			return
		}
	}

}

func syncRepo(op Options) error {
	srcRepo := op.Source
	dstRepo := op.Target

	httpClient := &http.Client{}
	if !strings.HasPrefix(srcRepo, "http") {
		srcRepo = "https://" + srcRepo
		httpClient.Transport = &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		}
	}
	url := fmt.Sprintf("%s/v2/_catalog?n=10000", srcRepo)

	resp, err := httpClient.Get(url)
	if err != nil {
		log.Fatalf("Failed to get repo: %v", err)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("Failed to read body: %v", err)
	}
	resp.Body.Close()

	var repos repo
	err = json.Unmarshal(body, &repos)
	if err != nil {
		log.Fatalf("Failed to unmarshal body: %v", err)
	}
	if repos.Repositories == nil || len(repos.Repositories) == 0 {
		log.Fatalf("Failed to get repo: %v", err)
	}

	failedImages := map[string][]string{}

	for _, img := range repos.Repositories {
		srcImg := srcRepo + "/" + img
		dstImg := dstRepo + "/" + img
		log.Infof("Handle image %s", srcImg)

		// get tags
		tagsURL := fmt.Sprintf("%s/v2/%s/tags/list", srcRepo, img)
		resp, err = httpClient.Get(tagsURL)
		if err != nil {
			log.Errorf("Failed to get tags: %v", err)
			failedImages[srcImg] = []string{fmt.Sprintf("Failed to get tags: %v", err)}
			continue
		}

		if err != nil {
			log.Warnf("%s get tags failed %s", img, err.Error())
			failedImages[srcImg] = []string{fmt.Sprintf("%s get tags failed %s", img, err.Error())}
			continue
		}

		body, err = io.ReadAll(resp.Body)
		if err != nil {
			log.Warnf("%s get tags failed %s", img, err.Error())
			failedImages[srcImg] = []string{fmt.Sprintf("%s get tags failed %s", img, err.Error())}
			continue
		}
		resp.Body.Close()
		tags := tagResponse{Tags: []string{}}
		err = json.Unmarshal(body, &tags)
		if err != nil {
			log.Warnf("%s unmarshal tags failed %s", img, err.Error())
			failedImages[srcImg] = []string{fmt.Sprintf("%s unmarshal tags failed %s", img, err.Error())}
			continue
		}

		if tags.Tags == nil || len(tags.Tags) == 0 {
			log.Warnf("%s get tags failed", img)
			failedImages[srcImg] = []string{fmt.Sprintf("%s get tags failed", img)}
			continue
		}

		for _, tag := range utils.ReverseArray(tags.Tags) {
			srcTag := srcImg + ":" + tag
			dstTag := dstImg + ":" + tag

			syncImageOp := op
			syncImageOp.Source = op.Source + "/" + img + ":" + tag
			syncImageOp.Target = op.Target + "/" + img + ":" + tag

			if strings.HasPrefix(syncImageOp.Source, "http") {
				syncImageOp.Source = strings.TrimPrefix(syncImageOp.Source, "http://")
			}
			if strings.HasPrefix(syncImageOp.Source, "https") {
				syncImageOp.Source = strings.TrimPrefix(syncImageOp.Source, "https://")
			}

			if strings.HasPrefix(syncImageOp.Target, "http") {
				syncImageOp.Target = strings.TrimPrefix(syncImageOp.Target, "http://")
			}
			if strings.HasPrefix(syncImageOp.Target, "https") {
				syncImageOp.Target = strings.TrimPrefix(syncImageOp.Source, "https://")
			}

			log.Infof("Sync image %s to %s", syncImageOp.Source, syncImageOp.Target)

			arch := []string{"amd64", "arm64"}
			err = syncRepoImage(syncImageOp, arch)
			if err != nil {
				log.BKEFormat(log.ERROR, fmt.Sprintf("Sync image %s to %s error %s", srcTag, dstTag, err.Error()))
				failedImages[syncImageOp.Source] = []string{fmt.Sprintf("Sync image %s to %s error %s", srcTag, dstTag, err.Error())}
				continue
			}
		}

	}

	if len(failedImages) > 0 {
		for key, value := range failedImages {
			log.BKEFormat(log.WARN, fmt.Sprintf("Failed to sync image %s %v", key, value))
		}
		return nil
	}

	return nil
}

func syncRepoImage(newOp Options, arch []string) error {
	imageAddress := newOp.Source
	targetAddress := newOp.Target
	if len(arch) == 1 {
		op := Options{
			MultiArch:     false,
			SrcTLSVerify:  newOp.SrcTLSVerify,
			DestTLSVerify: newOp.DestTLSVerify,
		}
		op.Arch = arch[0]
		op.Source = imageAddress
		op.Target = targetAddress

		log.BKEFormat(log.NIL, fmt.Sprintf("Sync image %s to %s", imageAddress, targetAddress))
		if err := CopyRegistry(op); err != nil {
			log.BKEFormat(log.WARN, fmt.Sprintf("Sync image %s to %s error %s", imageAddress, targetAddress, err.Error()))
			imageAddress = imageAddress + "-" + arch[0]
			log.BKEFormat(log.NIL, fmt.Sprintf("Sync image %s to %s", imageAddress, targetAddress))
			op.Source = imageAddress
			if err = CopyRegistry(op); err != nil {
				log.BKEFormat(log.WARN, fmt.Sprintf("Sync image %s to %s error %s", imageAddress, targetAddress, err.Error()))
				return err
			}
		}
		return nil
	}

	op := Options{
		MultiArch:     newOp.MultiArch,
		SrcTLSVerify:  newOp.SrcTLSVerify,
		DestTLSVerify: newOp.DestTLSVerify,
	}
	op.Source = imageAddress
	op.Target = targetAddress
	err := CopyRegistry(op)
	if err == nil {
		return nil
	}
	log.BKEFormat(log.WARN, fmt.Sprintf("Sync image %s to %s error %s by regitry", imageAddress, targetAddress,
		err.Error()))
	img := []ImageArch{}
	op.MultiArch = false
	for _, ar := range arch {
		op.Arch = ar
		op.Source = imageAddress + "-" + ar
		op.Target = targetAddress + "-" + ar
		if err := CopyRegistry(op); err != nil {
			log.BKEFormat(log.ERROR, fmt.Sprintf("Sync image %s to %s error %s , arch %s", imageAddress,
				targetAddress, ar, err.Error()))
			return err
		}
		img = append(img, ImageArch{
			Name:         op.Target,
			OS:           "linux",
			Architecture: ar,
		})
	}
	if err := CreateMultiArchImage(img, targetAddress); err != nil {
		log.BKEFormat(log.ERROR, fmt.Sprintf("Tche creation of multiple schema images manifests fails %s %s", targetAddress, err.Error()))
		return err
	}
	return nil
}

// CopyRegistry copies an image from a source to a destination.
func CopyRegistry(op Options) error {
	imageAddress := op.Source
	targetAddress := op.Target

	// 只为没有任何 transport 前缀的引用添加 docker://
	if !hasTransportPrefix(imageAddress) {
		imageAddress = fmt.Sprintf("docker://%s", imageAddress)
	}
	if !hasTransportPrefix(targetAddress) {
		targetAddress = fmt.Sprintf("docker://%s", targetAddress)
	}

	ctx := context.Background()
	srcRef, err := alltransports.ParseImageName(imageAddress)
	if err != nil {
		return fmt.Errorf("invalid source name %s: %v", op.Source, err)
	}
	destRef, err := alltransports.ParseImageName(targetAddress)
	if err != nil {
		return fmt.Errorf("invalid destination name %s: %v", targetAddress, err)
	}
	imageListSelection := copy.CopySystemImage
	if op.MultiArch {
		imageListSelection = copy.CopyAllImages
	}

	policy := &signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}}
	policyContext, err := signature.NewPolicyContext(policy)
	if err != nil {
		return fmt.Errorf("error loading trust policy: %v", err)
	}
	var signers []*signer.Signer
	var passphrase string
	var signIdentity reference.Named = nil
	var stdout = os.Stdout
	var manifestType string
	var encLayers *[]int
	var encConfig *encconfig.EncryptConfig
	var decConfig *encconfig.DecryptConfig
	sourceCtx, err := newSystemContext()
	if err != nil {
		return err
	}
	if op.SrcTLSVerify {
		sourceCtx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(true)
		sourceCtx.DockerDaemonInsecureSkipTLSVerify = true
	}
	destinationCtx, err := newSystemContext()
	if err != nil {
		return err
	}
	if op.DestTLSVerify {
		destinationCtx.DockerInsecureSkipTLSVerify = types.NewOptionalBool(true)
		destinationCtx.DockerDaemonInsecureSkipTLSVerify = true
	}
	if len(op.Arch) > 0 {
		sourceCtx.ArchitectureChoice = op.Arch
		destinationCtx.ArchitectureChoice = op.Arch
	}

	return retry.IfNecessary(ctx, func() error {
		_, err = copy.Image(ctx, policyContext, destRef, srcRef, &copy.Options{
			RemoveSignatures:                 false,
			Signers:                          signers,
			SignBy:                           "",
			SignPassphrase:                   passphrase,
			SignBySigstorePrivateKeyFile:     "",
			SignSigstorePrivateKeyPassphrase: []byte(passphrase),
			SignIdentity:                     signIdentity,
			ReportWriter:                     stdout,
			SourceCtx:                        sourceCtx,
			DestinationCtx:                   destinationCtx,
			ForceManifestMIMEType:            manifestType,
			ImageListSelection:               imageListSelection,
			PreserveDigests:                  false,
			OciDecryptConfig:                 decConfig,
			OciEncryptLayers:                 encLayers,
			OciEncryptConfig:                 encConfig,
		})
		if err != nil {
			log.BKEFormat(log.ERROR, fmt.Sprintf("exec sync image %s to %s error %s", imageAddress,
				targetAddress, err.Error()))
			return err
		}
		return nil
	}, &retry.Options{
		MaxRetry: 9,
		Delay:    1,
	})
}

// hasTransportPrefix checks if the image reference already has a transport prefix
func hasTransportPrefix(ref string) bool {
	transports := []string{
		"docker://",
		"oci:",
		"dir:",
		"docker-archive:",
		"oci-archive:",
		"docker-daemon:",
		"containers-storage:",
	}
	for _, transport := range transports {
		if strings.HasPrefix(ref, transport) {
			return true
		}
	}
	return false
}

// newSystemContext returns a *types.SystemContext corresponding to opts.
// It is guaranteed to return a fresh instance, so it is safe to make additional updates to it.
func newSystemContext() (*types.SystemContext, error) {
	ctx := &types.SystemContext{
		RegistriesDirPath:        "",
		ArchitectureChoice:       "",
		OSChoice:                 "",
		VariantChoice:            "",
		SystemRegistriesConfPath: "",
		BigFilesTemporaryDir:     "",
		DockerRegistryUserAgent:  "bke/v1.0.0",
	}

	ctx.DockerCertPath = ""
	ctx.OCISharedBlobDirPath = ""
	ctx.AuthFilePath = os.Getenv("REGISTRY_AUTH_FILE")
	ctx.DockerDaemonHost = ""
	ctx.DockerDaemonCertPath = ""
	return ctx, nil
}
